package org.geogebra.common.sound;

import org.geogebra.common.kernel.geos.GeoFunction;
import org.geogebra.common.util.debug.Log;

/**
 * Class for playing function-generated sounds.
 * 
 * @author Laszlo Gal
 *
 */
public abstract class FunctionSound {

	protected static final int DEFAULT_SAMPLE_RATE = 8000;
	protected static final int DEFAULT_BIT_RATE = 8;
	private int mBitDepth;
	private int mSampleRate;

	// set maximum volume to 100% of external volume setting
	private int maxVolume = 100;

	// sound function fields
	private volatile GeoFunction f;
	private double tMin;
	private double tMax;
	private double t; // records current time, used with pause/resume

	private double samplePeriod;
	private byte[] buf;

	/**
	 * Constructs instance of FunctionSound
	 */
	public FunctionSound() {
		mBitDepth = DEFAULT_BIT_RATE;
		mSampleRate = DEFAULT_SAMPLE_RATE;
	}

	/**
	 * Initializes instances of AudioFormat and SourceDataLine
	 * 
	 * @param sampleRate
	 *            = 8000, 16000, 11025, 16000, 22050, or 44100
	 * @param bitDepth
	 *            = 8 or 16
	 * @return whether params are valid
	 */
	protected boolean initStreamingAudio(int sampleRate, int bitDepth) {

		if (sampleRate != 8000 && sampleRate != 16000 && sampleRate != 11025
				&& sampleRate != 22050 && sampleRate != 44100) {
			return false;
		}

		if (bitDepth != 8 && bitDepth != 16) {
			return false;
		}

		this.mSampleRate = sampleRate;
		this.mBitDepth = bitDepth;

		return true;
	}

	/**
	 * Plays a sound generated by the time valued GeoFunction f(t), from t = min
	 * to t = max in seconds. The function is assumed to have range [-1,1] and
	 * will be clipped to this range otherwise.
	 * 
	 * @param geoFunction
	 *            function
	 * @param min
	 *            minimum x
	 * @param max
	 *            maximum x
	 */
	public void playFunction(GeoFunction geoFunction, double min, double max) {
		playFunction(geoFunction, min, max, DEFAULT_SAMPLE_RATE,
				DEFAULT_BIT_RATE);
	}

	/**
	 * Plays a sound generated by the time valued GeoFunction f(t), from t = min
	 * to t = max in seconds. The function is assumed to have range [-1,1] and
	 * will be clipped to this range otherwise.
	 * 
	 * @param geoFunction
	 *            function
	 * @param min
	 *            minimum x
	 * @param max
	 *            maximum x
	 * @param sampleRate
	 *            samples per second
	 * @param bitDepth
	 *            depth 8 or 16
	 */
	public abstract void playFunction(GeoFunction geoFunction,
			double min, double max, int sampleRate,
			int bitDepth);

	/**
	 * @param geoFunction
	 *            function
	 * @param min
	 *            min time
	 * @param max
	 *            max time
	 * @param sampleRate
	 *            sample rate
	 * @param bitDepth
	 *            depth
	 * @return whether audio initialization fails
	 */
	public boolean checkFunction(final GeoFunction geoFunction,
			final double min, final double max, final int sampleRate,
			final int bitDepth) {
		f = geoFunction;
		this.tMin = min;
		this.tMax = max;
		return (sampleRate == DEFAULT_SAMPLE_RATE && bitDepth == DEFAULT_BIT_RATE)
				|| initStreamingAudio(sampleRate, bitDepth);
	}

	/**
	 * Pauses/resumes sound generation
	 * 
	 * @param doPause
	 *            whether to pause or resume
	 */
	public abstract void pause(boolean doPause);

	public int getBitDepth() {
		return mBitDepth;
	}

	public void setBitDepth(int bitDepth) {
		this.mBitDepth = bitDepth;
	}

	public int getSampleRate() {
		return mSampleRate;
	}

	public void setSampleRate(int sampleRate) {
		this.mSampleRate = sampleRate;
	}

	/**
	 * Fills the internal buffer with sound data generated by time-valued
	 * GeoFunction f(t) starting at time t. Uses 8-bit mono samples.
	 * 
	 * @param time
	 *            time
	 */
	protected void loadBuffer8(double time) {
		double value;
		for (int k = 0; k < getBuf().length; k++) {
			value = getF().value(time + 1.0 * k * getSamplePeriod());
			// clip sound data
			if (value > 1.0) {
				value = 1.0;
			}
			if (value < -1.0) {
				value = -1.0;
			}

			value = value * getMaxVolume();

			// make sure rounding works when truncated to short/byte
			if (value > 0) {
				value += 0.5;
			} else if (value < 0) {
				value -= 0.5;
			}

			getBuf()[k] = (byte) value;
		}
	}

	/**
	 * Fills the internal buffer with sound data generated by time-valued
	 * GeoFunction f(t) starting at time t. Uses 16-bit mono, signed, big-endian
	 * samples.
	 * 
	 * @param time
	 *            time
	 */
	protected void loadBuffer16(double time) {
		double value;
		for (int k = 0; k < getBuf().length / 2; k++) {
			if (k < 5 || k > getBuf().length / 2 - 6) {
				Log.debug(k + " " + (time + 1.0 * k * getSamplePeriod()));
			}
			value = getF().value(time + 1.0 * k * getSamplePeriod());
			// clip sound data
			if (value > 1.0) {
				value = 1.0;
			}
			if (value < -1.0) {
				value = -1.0;
			}

			value = value * getMaxVolume();

			// make sure rounding works when truncated to short/byte
			if (value > 0) {
				value += 0.5;
			} else if (value < 0) {
				value -= 0.5;
			}

			short sample = (short) value;
			getBuf()[2 * k] = (byte) (sample & 0xff);
			getBuf()[2 * k + 1] = (byte) ((sample >> 8) & 0xff);
		}
	}

	/**
	 * Shapes ends of waveform to fade sound data TODO: is this actually
	 * working?
	 * 
	 * @param peakValue
	 *            peak value
	 * @param isFadeOut
	 *            whether to fade out
	 * @return amplitude values
	 */
	protected byte[] getFadeBuffer(short peakValue, boolean isFadeOut) {

		int numSamples = getSampleRate() / 100;
		byte[] fadeBuf = new byte[getBitDepth() == 8 ? numSamples
				: 2 * numSamples];

		double delta = 1.0 * peakValue / numSamples;
		if (isFadeOut) {
			delta = -delta;
		}

		short value = isFadeOut ? peakValue : 0;

		for (int k = 0; k < numSamples; k++) {
			if (getBitDepth() == 8) {
				fadeBuf[k] = (byte) value;
			} else {
				fadeBuf[2 * k] = (byte) (value & 0xff);
				fadeBuf[2 * k + 1] = (byte) ((value >> 8) & 0xff);
			}

			value += delta;
		}

		return fadeBuf;
	}

	public GeoFunction getF() {
		return f;
	}

	public void setF(GeoFunction f) {
		this.f = f;
	}

	public double getMin() {
		return tMin;
	}

	public void setMin(double min) {
		this.tMin = min;
	}

	/**
	 * @return max time
	 */
	public double getMax() {
		return tMax;
	}

	/**
	 * @param max
	 *            max time
	 */
	public void setMax(double max) {
		this.tMax = max;
	}

	public double getT() {
		return t;
	}

	public void setT(double t) {
		this.t = t;
	}

	public double getSamplePeriod() {
		return samplePeriod;
	}

	public void setSamplePeriod(double samplePeriod) {
		this.samplePeriod = samplePeriod;
	}

	public byte[] getBuf() {
		return buf;
	}

	public void setBuf(byte[] buf) {
		this.buf = buf;
	}

	public int getBufLength() {
		return buf.length;
	}

	public int getMaxVolume() {
		return maxVolume;
	}

	public void setMaxVolume(int maxVolume) {
		this.maxVolume = maxVolume;
	}
}
